package cn.tedu.lbs.marketing.web.service;

import cn.tedu.lbs.basic.api.BasicApi;
import cn.tedu.lbs.marketing.web.repository.ActivityRepository;
import cn.tedu.lbs.marketing.web.repository.CouponCodeRepository;
import cn.tedu.lbs.marketing.web.repository.CouponRepository;
import cn.tedu.lbs.marketing.web.repository.UserCouponRepository;
import cn.tedu.lbs.marketing.web.utils.AuthenticationContextUtil;
import cn.tedu.lbs.stock.api.StockApi;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tarena.lbs.attach.api.AttachApi;
import com.tarena.lbs.base.common.utils.Asserts;
import com.tarena.lbs.base.protocol.exception.BusinessException;
import com.tarena.lbs.base.protocol.pager.PageResult;
import com.tarena.lbs.common.passport.principle.UserPrinciple;
import com.tarena.lbs.pojo.attach.dto.AttachQrDTO;
import com.tarena.lbs.pojo.attach.param.AttachQRParam;
import com.tarena.lbs.pojo.basic.dto.UserDTO;
import com.tarena.lbs.pojo.marketing.param.UserCouponsParam;
import com.tarena.lbs.pojo.marketing.po.ActivityPO;
import com.tarena.lbs.pojo.marketing.po.CouponCodePO;
import com.tarena.lbs.pojo.marketing.po.CouponPO;
import com.tarena.lbs.pojo.marketing.po.UserCouponsPO;
import com.tarena.lbs.pojo.marketing.query.UserCouponCodeQuery;
import com.tarena.lbs.pojo.marketing.query.UserCouponQuery;
import com.tarena.lbs.pojo.marketing.vo.UserCouponsVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcException;
import org.omg.SendingContext.RunTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UserCouponService {
    @DubboReference
    private BasicApi basicApi;
    @DubboReference
    private StockApi stockApi;
    @Autowired
    private ActivityRepository activityRepository;
    @Autowired
    private CouponRepository couponRepository;
    @Autowired
    private UserCouponRepository userCouponRepository;
    @Autowired
    private CouponCodeRepository couponCodeRepository;
    @Autowired
    private RedisTemplate redisTemplate;
    /*@Autowired
    private ThreadPoolTaskExecutor executor;*/
    @Transactional(rollbackFor = Exception.class)
    public void receiveCoupon(UserCouponsParam param) throws BusinessException, ExecutionException, InterruptedException {
        //1.校验用户userId合法性
        CompletableFuture<Void> userFuture = CompletableFuture.supplyAsync((Supplier<Void>) () -> {
            try {
                checkUser(param.getUserId());
            } catch (BusinessException e) {
                throw new RuntimeException(e);
            }
            return null;
        });
        CompletableFuture<Void> activityFutre = CompletableFuture.supplyAsync((Supplier<Void>) () -> {
            try {
                checkActivity(param.getActivityId(), param.getUserId());
            } catch (BusinessException e) {
                throw new RuntimeException(e);
            }
            return null;
        });
        CompletableFuture<CouponPO> couponFuture=CompletableFuture.supplyAsync(() -> {
            try {
                return checkAndGetCoupon(param.getCouponId(),param.getUserId());
            } catch (BusinessException e) {
                throw new RuntimeException(e);
            }
        });
        //如果子线程执行过程 后续没有同步等待需求,当前代码 就可以直接执行了.
        //但是 领取执行操作需要等待上面3个检查结果的,所以在主线程 同步阻塞 等待所有子线程完成
        CompletableFuture<Void> allFuture = CompletableFuture.allOf(userFuture, activityFutre, couponFuture);
        userFuture.get();
        activityFutre.get();
        CouponPO coupon = couponFuture.get();
        //拿到子线程的返回结果
        //4.验证无误 开始领取 将整个领取的过程 包装到一个锁里 保证步骤原子级执行
        //VERSION1 只有对当前优惠券领取抢到锁的线程 才可以进入doReceiveCoupon setnx属于String set get append
        //setnx set non-exists 如果key不存在 才set存储
        ValueOperations stringOps = redisTemplate.opsForValue();
        String lockKey="lbs:marketing:coupon:"+param.getCouponId();
        String lockValue=UUID.randomUUID().toString();
        Boolean getKey = false;
        int index=0;
        do{
            if (index!=0&&index<=5){
                //当前线程不是第一次抢 睡 200毫秒
                try{
                    Thread.sleep(200);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }else if(index!=0&&index>5){
                return;
            }
            getKey=stringOps.setIfAbsent(lockKey, lockValue,5, TimeUnit.SECONDS);
            index++;
        }while(!getKey);
        try{
            doReceiveCoupon(param, coupon);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //判断锁的归属 之后再删除
            String value= (String) stringOps.get(lockKey);
            if (StringUtils.equals(lockValue,value)){
                redisTemplate.delete(lockKey);
            }
        }
    }

    private void doReceiveCoupon(UserCouponsParam param, CouponPO coupon) throws BusinessException {
        long start=System.currentTimeMillis();
        log.info("开始领取优惠券");
        //1.查询优惠券是否剩余库存充足 >0 每个人每次请求只能另一个
        checkStockEnough(coupon.getId());
        //2.给当前开始领取 生成一个 懒加载的券码couponCode status 0未分配 1已分配
        CouponCodePO couponCode=createAndGetCouponCode(coupon);
        //3.组织数据写入用户领取记录表
        createUserCoupons(param,coupon,couponCode);
        //4.修改券码状态
        updateCouponCodeStatus(couponCode.getId(),1);//已领取
        //5.远程调用领取扣减的优惠券库存
        reduceCouponStock(coupon.getId());
        log.info("用户领取优惠券结束");
        long end=System.currentTimeMillis();
        log.info("用户领取优惠券,执行时间:{}ms",end-start);
    }

    private void reduceCouponStock(Integer couponId) throws BusinessException {
        try{
            boolean result = stockApi.reduceCouponStock(couponId, 1);
            if (!result){
                throw new BusinessException("-2","扣减库存不成功");
            }
        }catch (RpcException e){
            //远程调用异常
            throw new BusinessException("-2","远程调用失败");
        }

    }

    private void updateCouponCodeStatus(Integer id, Integer status) {
        //update coupon_code set status=? where id=?
        CouponCodePO poParam=new CouponCodePO();
        poParam.setStatus(status);
        poParam.setId(id);
        couponCodeRepository.updateById(poParam);
    }

    private void createUserCoupons(UserCouponsParam param, CouponPO coupon, CouponCodePO couponCode) {
        UserCouponsPO po=new UserCouponsPO();
        po.setCouponValue(coupon.getDiscountValue());//优惠券面额
        po.setCouponId(param.getCouponId());
        po.setUserId(param.getUserId());
        po.setCouponCode(couponCode.getCouponCode());
        po.setActivityId(param.getActivityId());
        po.setReceiveChannel(param.getReceiveChannel());
        po.setShopId(param.getShopId());
        po.setCouponType(coupon.getCouponType());
        po.setCreateAt(new Date());
        po.setUpdateAt(new Date());
        po.setStatus(0);//未使用 0 1 已使用 2已过期
        userCouponRepository.save(po);
    }

    private CouponCodePO createAndGetCouponCode(CouponPO coupon) {
        //id couponId businessId code create update
        CouponCodePO po=new CouponCodePO();
        po.setCouponId(coupon.getId());
        po.setBusinessId(coupon.getBusinessId());
        po.setCouponCode(UUID.randomUUID().toString());
        po.setStatus(0);
        po.setCreateAt(new Date());
        po.setUpdateAt(new Date());
        couponCodeRepository.save(po);
        return po;
    }

    private void checkStockEnough(Integer couponId) throws BusinessException {
        Integer couponStock = stockApi.getCouponStock(couponId);
        //断言空
        Asserts.isTrue(couponStock==null,new BusinessException("-2","优惠券库存不存在"));
        //断言小于等于0
        Asserts.isTrue(couponStock<=0,new BusinessException("-2","优惠券库存不足"));
    }

    private CouponPO checkAndGetCoupon(Integer couponId,Integer userId)throws BusinessException {
        long start=System.currentTimeMillis();
        log.info("校验优惠券合法:{}",couponId);
        //1.判断存在
        CouponPO po = couponRepository.getById(couponId);
        Asserts.isTrue(po==null,new BusinessException("-2","优惠券不存在"));
        //2.用户领取上限 select count() from user_coupons where userId=? and couponId=?
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("coupon_id",couponId);
        long count = userCouponRepository.count(queryWrapper);
        //如果count值>= couponPO定义的上限
        Asserts.isTrue(count>=po.getUsageLimit(),new BusinessException("-2","用户领取优惠券已达上限"));
        log.info("校验优惠券合法:{},结果是正常的",couponId);
        long end=System.currentTimeMillis();
        log.info("校验优惠券合法,执行时间:{}ms",end-start);
        return po;
    }

    private void checkActivity(Integer activityId,Integer userId) throws BusinessException {
        long start=System.currentTimeMillis();
        log.info("校验活动合法:{}",activityId);
        //1.读取activityPO 断言空
        ActivityPO activityPO = activityRepository.getById(activityId);
        Asserts.isTrue(activityPO==null,new BusinessException("-2","活动不存在"));
        //时间范围 在start之后 在end之前
        Date startDate = activityPO.getStartDate();
        Date endDate = activityPO.getEndDate();
        Date now= new Date();
        boolean isOnTime=startDate.before(now)&&endDate.after(now);
        //断言是否在时间范围内
        Asserts.isTrue(!isOnTime,new BusinessException("-2","活动不在时间范围内"));
        //2.远程调用 读取用户所属人群列表 判断是否包含目标人群
        Integer targetGroupId = Integer.valueOf(activityPO.getTargetCustomer());
        List<Integer> userGroupIds = basicApi.getUserGroupIds(userId, activityPO.getBusinessId());
        //返回结果 如果是null 也人为用户可以领取 但是如果不为空 并且不包含目标人群id false断言抛异常结束
        if (userGroupIds!=null){
            Asserts.isTrue(!userGroupIds.contains(targetGroupId),new BusinessException("-2","用户不符合活动目标人群"));
        }
        log.info("校验活动合法:{},结果是正常的",activityId);
        long end=System.currentTimeMillis();
        log.info("校验活动合法,执行时间:{}ms",end-start);
    }

    private void checkUser(Integer userId) throws BusinessException {
        long start=System.currentTimeMillis();
        log.info("校验用户合法:{}",userId);
        //远程调用basicApi
        UserDTO userDTO = basicApi.userDetail(userId);
        //断言空
        Asserts.isTrue(userDTO==null,new BusinessException("-2","用户不存在"));
        log.info("校验用户合法:{},结果是正常的",userId);
        long end=System.currentTimeMillis();
        log.info("校验用户合法,执行时间:{}ms",end-start);
    }

    public PageResult<UserCouponsVO> myCoupons(UserCouponQuery couponQuery) throws BusinessException {
        //1.拿到userId 补充query
        Integer userId = getUserId();
        //2.拼接where条件 查询优惠券列表
        couponQuery.setUserId(userId);
        List<UserCouponsPO> pos=getMyCoupons(couponQuery);
        //UNDO 使用关联查询 更新 status=2
        //3.封装pageResult返回
        return assemblePageResult(pos);
    }

    private PageResult<UserCouponsVO> assemblePageResult(List<UserCouponsPO> pos) {
        PageResult<UserCouponsVO> pageResult=new PageResult<>(10,1);
        pageResult.setTotal(100l);
        if (CollectionUtils.isNotEmpty(pos)){
            pageResult.setObjects(pos.stream().map(po->{
                UserCouponsVO vo=new UserCouponsVO();
                BeanUtils.copyProperties(po,vo);
                return vo;
                    }
            ).collect(Collectors.toList()));
        }else{
            pageResult.setObjects(null);
        }
        return pageResult;
    }

    private List<UserCouponsPO> getMyCoupons(UserCouponQuery couponQuery) {
        QueryWrapper<UserCouponsPO> query=new QueryWrapper<>();
        query.eq("user_id",couponQuery.getUserId());
        query.eq("status",couponQuery.getStatus());
        //保证这个优惠券 未使用 同时没过期
        //select uc.* from user_coupons uc join coupon c on uc.coupon_id=c.id
        //where uc.user_id=? and uc.status=? and c.end_date>now()
        return userCouponRepository.list(query);
    }

    private Integer getUserId() throws BusinessException {
        //读取 认证 传递的userPrinciple 线程和过滤器线程相同
        UserPrinciple userPrinciple = AuthenticationContextUtil.get();
        Asserts.isTrue(userPrinciple==null,new BusinessException("-2","用户认证解析失败"));
        return userPrinciple.getId();
    }

    public UserCouponsVO detail(UserCouponCodeQuery query) throws BusinessException {
        //1.组织 获取userId
        Integer userId = getUserId();
        query.setUserId(userId);
        //2.查询用户领取记录po
        UserCouponsPO po=getUserCoupons(query);
        //3.转化vo 缺少二维码url地址
        if (po!=null){
            UserCouponsVO vo=new UserCouponsVO();
            BeanUtils.copyProperties(po,vo);
            //4.调用远程attach获取二维码 连接地址
            String qrUrl=generateQRUrl(vo);
            vo.setCouponUrl(qrUrl);
            return vo;
        }else{
            return null;
        }
    }
    @DubboReference
    private AttachApi attachApi;

    private String generateQRUrl(UserCouponsVO vo) {
        //在图片服务中 实现了一下一些操作
        //1.根据参数 生成二维码 内容也是扫码可见的东西 abc http://lbs.order.com/create/order/{couponCode}
        //2.生成数据库 描述信息 并且绑定当前业务
        //旁路缓存 流程 一 操作String 类型客户端
        ValueOperations<String,String> opsForValue = redisTemplate.opsForValue();
        String key="lbs:marketing:coupon:code:"+vo.getCouponCode();
        //二 获取缓存 判断命中
        String redisUrl = opsForValue.get(key);
        if (StringUtils.isNotEmpty(redisUrl)){
            log.info("当前优惠券码:{},缓存命中",vo.getCouponCode());
            //缓存命中
            return redisUrl;
        }else{
            log.info("当前优惠券码:{},缓存未命中",vo.getCouponCode());
            AttachQRParam param=new AttachQRParam();
            param.setContent("http://www.baidu.com?code="+vo.getCouponCode());
            //二维码类型 800 业务id vo.getId
            param.setBusinessType(800);
            param.setBusinessId(vo.getId());
            AttachQrDTO attachQrDTO = attachApi.generateQrCode(param);
            //三 为了后续访问顺利命中 存储一次缓存
            opsForValue.set(key,attachQrDTO.getUrl());
            return attachQrDTO.getUrl();
        }


    }

    private UserCouponsPO getUserCoupons(UserCouponCodeQuery query) {
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",query.getUserId());
        queryWrapper.eq("coupon_code",query.getCouponCode());
        return userCouponRepository.getOne(queryWrapper);
    }
}
